網路上已經有很多介紹Firestore細節的文章了,在這邊就快速的帶大家設置我們要的資料結構。
設定好Firestore後回到專案中。
如果Day15前半段提到的設定都有做到,那麼就可以直接使用Firestore的功能啦,還沒設定好的就回頭看一下Day15吧。
在firebase資料夾下新增「firestore_database.dart」。
要取得Firestore資料的程式碼非常簡單,而且這次我們不用再自己寫一個Bloc實作,因為它本身就是「Stream」了,只要使用StreamBuilder
去監聽它的資料狀態顯示對應的畫面即可。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:fluttube/home/comment_widget.dart';
StreamBuilder<QuerySnapshot> getComments(String movieId) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection('comments')
.where("movie_id", isEqualTo: movieId)
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData)
return Center(
child: CircularProgressIndicator(),
);
final int commentCount = snapshot.data.documents.length;
snapshot.data.documents
.sort((a, b) => b.data['time'].compareTo(a.data['time']));
if (commentCount > 0) {
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: commentCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document = snapshot.data.documents[index];
return commentWidget(
document['user_email'],
document['content'],
document['time'],
);
},
);
} else {
return Container(
padding: EdgeInsets.symmetric(vertical: 10.0),
alignment: Alignment.center,
child: Text(
'no comments...',
style: TextStyle(fontSize: 20),
),
);
}
},
);
}
void createRecord(String movieId, String email, String content) async {
await Firestore.instance.collection("comments").document().setData({
'movie_id': movieId,
'user_email': email,
'content': content,
'time': Timestamp.now()
});
}
Firestore.instance.collection('comments').where("movie_id", isEqualTo: movieId).snapshots()
這一行是從Firestore取資料的程式碼,指定要取得的集合然後用where
來過濾資料。
snapshot.data.documents.sort((a, b) => b.data['time'].compareTo(a.data['time']));
這一行是對取回來的資料依照留言時間的先後做排序,越新的留言排在越上面。
在home資料夾下新增「comment_widget.dart」。
使用timeago套件協助轉換取回來的TimeStamp值,
import 'package:flutter/material.dart';
import 'package:timeago/timeago.dart' as timeago;
Widget commentWidget(String email, String content, var time) {
return Container(
padding: EdgeInsets.symmetric(vertical: 5),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Colors.black,
width: 3.0,
))),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
email,
style: TextStyle(
fontSize: 20,
color: Colors.lightBlueAccent,
fontWeight: FontWeight.bold),
),
Text(
timeago.format(time.toDate()),
style: TextStyle(
fontSize: 10,
color: Colors.grey,
),
),
],
),
SizedBox(
height: 10.0,
),
Container(
padding: EdgeInsets.only(left: 10.0),
alignment: Alignment.topLeft,
child: Text(
content,
style: TextStyle(fontSize: 16),
),
),
],
));
}
修改成以下程式碼:
import 'package:flutter/material.dart';
import '../youtube/youtube.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'trailer_widget.dart';
import '../firebase/firestore_database.dart';
import '../firebase/user_repository.dart';
class MovieDetailPage extends StatefulWidget {
final posterPath;
final overview;
final releaseDate;
final title;
final voteAverage;
final movieId;
MovieDetailPage(
{Key key,
this.posterPath,
this.overview,
this.releaseDate,
this.title,
this.voteAverage,
this.movieId})
: super(key: key);
@override
_MovieDetailPageState createState() => _MovieDetailPageState();
}
class _MovieDetailPageState extends State<MovieDetailPage> {
String get posterPath => widget.posterPath;
String get overview => widget.overview;
String get releaseDate => widget.releaseDate;
String get title => widget.title;
String get voteAverage => widget.voteAverage.toString();
String get movieId => widget.movieId.toString();
bool isOverviewSelected = false;
bool isSubmitEnable = false;
YoutubeBloc _youtubeBloc;
YoutubeRepository _youtubeRepository;
final TextEditingController _inputController = TextEditingController();
@override
void initState() {
_youtubeRepository = YoutubeRepository();
_youtubeBloc = YoutubeBloc(youtubeRepository: _youtubeRepository);
_youtubeBloc.dispatch(SearchYoutubeEvent("$title 預告片"));
_inputController.addListener(_onInputChanged);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
bottom: false,
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: 200.0,
floating: false,
elevation: 0.0,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
"https://image.tmdb.org/t/p/w500${posterPath}",
fit: BoxFit.cover,
)),
),
SliverList(
delegate: SliverChildListDelegate([
Container(margin: EdgeInsets.only(top: 5.0)),
Text(
title,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
),
),
Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
Row(
children: <Widget>[
Icon(
Icons.favorite,
color: Colors.red,
),
Container(
margin: EdgeInsets.only(left: 1.0, right: 1.0),
),
Text(
voteAverage,
style: TextStyle(
fontSize: 18.0,
),
),
Container(
margin: EdgeInsets.only(left: 10.0, right: 50.0),
),
Text(
"上映日期:${releaseDate}",
style: TextStyle(
fontSize: 16.0,
),
),
],
),
Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
isOverviewSelected
? GestureDetector(
onTap: () => setState(() {
isOverviewSelected = !isOverviewSelected;
}),
child: Text(overview),
)
: GestureDetector(
onTap: () => setState(() {
isOverviewSelected = !isOverviewSelected;
}),
child: Column(
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints(maxHeight: 200),
child: Text(
overview,
softWrap: true,
overflow: TextOverflow.visible,
maxLines: 2,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.arrow_drop_down),
Text('閱讀全文'),
],
)
],
),
),
Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
Text(
"Trailer",
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
),
),
Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
Container(
child: BlocBuilder(
bloc: _youtubeBloc,
builder: (context, state) {
if (state is YoutubeSuccessState) {
return trailerWidget(state.ytResult);
}
return Center(child: CircularProgressIndicator());
},
),
),
Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
Text(
"Comments",
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
),
),
ListTile(
title: TextField(
controller: _inputController,
decoration: InputDecoration(
icon: Icon(Icons.comment), labelText: '留言'),
),
trailing: IconButton(
onPressed: isSubmitEnable
? () async {
String email =
await new UserRepository().getUser();
createRecord(
movieId, email, _inputController.text);
_inputController.clear();
}
: null,
icon: Icon(Icons.subdirectory_arrow_left)),
),
Container(
child: getComments(movieId),
)
]),
)
],
)),
);
}
void _onInputChanged() {
if (_inputController.text.isNotEmpty) {
setState(() {
isSubmitEnable = true;
});
} else {
setState(() {
isSubmitEnable = false;
});
}
}
@override
void dispose() {
_inputController.dispose();
super.dispose();
}
}
和昨天的相比,我們多加了TextField
讓使用者可以輸入他的留言,當按下送出後就使用firestore_database裡面的createRecord
新增一筆留言資料。
把當前的電影id傳給getComments
回傳StreamBuilder widget顯示使用者留言。
最後記得在dispose()
把inputController給關閉。
使用Firebase能大幅度減少開發的時間,當然如果是大型專案還是使用穩固的資料庫比較適合。不過單以今天的留言功能來說,大概半小時~一個小時就能完成了,真的是很方便呢。
不過現在的留言長的實在太醜了,我想加上使用者的大頭貼應該會好看點,所以明後兩天就使用Firebase提供的其他服務來幫助我們達成這項任務吧。
完整程式碼在這裡-> FlutTube Github